﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;

namespace IslandInterloper.Engine
{
    public interface IGame
    {
        XElement DoCommand(XElement command);
        XElement ToXElement(XName xName);
    }
    internal class Game:IGame
    {
        #region Config
        private Config config;
        public Config Config
        {
            get
            {
                return config;
            }
        }
        #endregion
        #region Commodities
        private Commodities commodities;
        public Commodities Commodities
        {
            get
            {
                return commodities;
            }
        }
        #endregion
        #region Crewmembers
        private Crewmembers crewmembers;
        public Crewmembers Crewmembers
        {
            get
            {
                return crewmembers;
            }
        }
        #endregion
        #region Ships
        private Ships ships;
        public Ships Ships
        {
            get
            {
                return ships;
            }
        }
        #endregion
        #region Avatar
        private Avatar avatar;
        public Avatar Avatar
        {
            get
            {
                return avatar;
            }
        }
        #endregion
        #region Islands
        private Islands islands;
        public Islands Islands
        {
            get
            {
                return islands;
            }
        }
        #endregion
        #region Constructor
        private Game() { }
        public Game(XElement element)
        {
            SetCommandHandlers();
            FromXElement(element);
        }
        #endregion
        #region Serialization
        private void FromXElement(XElement element)
        {
            config = new Config(this,element.Element(GameXNames.config));
            commodities = new Commodities(this, element.Element(GameXNames.commodities));
            crewmembers = new Crewmembers(this, element.Element(GameXNames.crewmembers));
            ships = new Ships(this, element.Element(GameXNames.ships));
            avatar = new Avatar(this, element.Element(GameXNames.avatar));
            islands = new Islands(this, element.Element(GameXNames.islands));
        }
        public XElement ToXElement(XName xName)
        {
            XElement result = new XElement(xName,
                config.ToXElement(GameXNames.config),
                commodities.ToXElement(GameXNames.commodities),
                crewmembers.ToXElement(GameXNames.crewmembers),
                ships.ToXElement(GameXNames.ships),
                avatar.ToXElement(GameXNames.avatar),
                islands.ToXElement(GameXNames.islands)
                );
            return result;
        }
        #endregion
        #region Game Command Infrastructure
        private delegate XElement GameCommandHandler(XElement command);
        private Dictionary<AvatarState, Dictionary<Command.CommandValue, GameCommandHandler>> commandDispatcher = new Dictionary<AvatarState, Dictionary<Command.CommandValue, GameCommandHandler>>();
        private void SetCommandHandler(AvatarState theAvatarState, Command.CommandValue theGameCommand, GameCommandHandler theGameCommandHandler)
        {
            if (!commandDispatcher.ContainsKey(theAvatarState))
            {
                commandDispatcher.Add(theAvatarState, new Dictionary<Command.CommandValue, GameCommandHandler>());
            }
            if (commandDispatcher[theAvatarState].ContainsKey(theGameCommand))
            {
                commandDispatcher[theAvatarState][theGameCommand] = theGameCommandHandler;
            }
            else
            {
                commandDispatcher[theAvatarState].Add(theGameCommand, theGameCommandHandler);
            }
        }
        private void SetCommandHandler(Command.CommandValue theGameCommand, GameCommandHandler theGameCommandHandleer)
        {
            foreach (AvatarState theState in Enum.GetValues(typeof(AvatarState)))
            {
                SetCommandHandler(theState, theGameCommand, theGameCommandHandleer);
            }

        }
        public XElement DoCommand(XElement command)
        {
            if (command == null)
            {
                return Result.CreateGenericFailureResult("Invalid command.");
            }
            bool success;
            Command.CommandValue value = Command.GetCommand(command, out success);
            if (!success)
            {
                return Result.CreateGenericFailureResult("Invalid command.");
            }
            if (commandDispatcher.ContainsKey(Avatar.State) && commandDispatcher[Avatar.State].ContainsKey(value))
            {
                return commandDispatcher[Avatar.State][value](command);
            }
            else
            {
                return Result.CreateGenericFailureResult("Invalid command.");
            }
        }
        #endregion
        #region Game Commands
        private void SetCommandHandlers()
        {
            SetCommandHandler(Command.CommandValue.GetAvatarInformation, GetAvatarInformationCommandHandler);
            SetCommandHandler(Command.CommandValue.GetCommodityInformation, GetCommodityInformationCommandHandler);
            SetCommandHandler(Command.CommandValue.GetIslandInformation, GetIslandInformationCommandHandler);
            SetCommandHandler(Command.CommandValue.ToggleIslandBookmark, ToggleBookmarkIslandCommandHandler);
            SetCommandHandler(Command.CommandValue.AnnotateIsland, AnnotateIslandCommandHandler);
            SetCommandHandler(Command.CommandValue.GetAvatarSpeedList, GetAvatarSpeedListCommandHandler);
            SetCommandHandler(Command.CommandValue.GetShipInformation, GetShipInformationCommandHandler);
            SetCommandHandler(Command.CommandValue.GetCrewmemberInformation, GetCrewmemberInformationCommandHandler);

            SetCommandHandler(AvatarState.Docked,Command.CommandValue.Undock, UndockCommandHandler);
            SetCommandHandler(AvatarState.Docked, Command.CommandValue.BuyCommodity, BuyCommodityCommandHandler);
            SetCommandHandler(AvatarState.Docked, Command.CommandValue.SellCommodity, SellCommodityCommandHandler);
            SetCommandHandler(AvatarState.Uninitialized, Command.CommandValue.StartGame, StartGameCommandHandler);
            SetCommandHandler(AvatarState.AtSea, Command.CommandValue.GetDockableIslandList, GetDockableIslandListCommandHandler);
            SetCommandHandler(AvatarState.AtSea, Command.CommandValue.Dock, DockCommandHandler);
            SetCommandHandler(AvatarState.AtSea, Command.CommandValue.GetNearByIslandList, GetNearByIslandListCommandHandler);
            SetCommandHandler(AvatarState.AtSea, Command.CommandValue.GetAvatarSpeedInformation, GetAvatarSpeedInformationCommandHandler);
            SetCommandHandler(AvatarState.AtSea, Command.CommandValue.SetSpeed, SetAvatarSpeedCommandHandler);
            SetCommandHandler(AvatarState.AtSea, Command.CommandValue.SetHeading, SetHeadingCommandHandler);
            SetCommandHandler(AvatarState.AtSea, Command.CommandValue.Sail, SailCommandHandler);
            SetCommandHandler(AvatarState.AtSea, Command.CommandValue.SetBoundForIsland, SetBoundForCommandHandler);
            SetCommandHandler(AvatarState.AtSea, Command.CommandValue.GetKnownIslandList, GetKnownIslandListCommandHandler);
        }
        private XElement GetCrewmemberInformationCommandHandler(XElement command)
        {
            string crewmember = Command.GetCrewmember(command);
            return Result.CreateCrewmemberInformationResult(Crewmembers[crewmember]);
        }
        private XElement BuyCommodityCommandHandler(XElement command)
        {
            if (Avatar.State != AvatarState.Docked)
            {
                return Result.CreateGenericFailureResult("Cannot buy commodities unless the avatar is docked.");
            }
            string commodity = Command.GetCommodity(command);
            if (!Commodities[commodity].Traded)
            {
                return Result.CreateGenericFailureResult("Cannot buy commodities if it is not traded.");
            }
            double units = Command.GetUnits(command);
            if(units<=0.0)
            {
                return Result.CreateGenericFailureResult("Must purchase a positive number of units.");
            }
            Island island = Islands[Avatar.DockedIslandID];
            double unitPrice = island.Commodities[commodity].BuyUnitPrice;
            if (units > Avatar.Inventory[commodity].MaximumPurchase(unitPrice))
            {
                return Result.CreateGenericFailureResult("You cannot purchase that many units.");
            }
            double totalPrice = unitPrice * units;
            double currentCost = Avatar.Inventory[commodity].Units * Avatar.Inventory[commodity].CostPerUnit;
            island.Commodities[commodity].BuyUnits(units);
            Avatar.Inventory[commodity].Units += units;
            Avatar.Inventory[commodity].CostPerUnit = (totalPrice + currentCost) / Avatar.Inventory[commodity].Units;
            Avatar.Inventory[Config.CurrencyCommodity].Units -= totalPrice;
            return Result.CreateGenericSuccessResult();
        }
        private XElement SellCommodityCommandHandler(XElement command)
        {
            if (Avatar.State != AvatarState.Docked)
            {
                return Result.CreateGenericFailureResult("Cannot sell commodities unless the avatar is docked.");
            }
            string commodity = Command.GetCommodity(command);
            if (!Commodities[commodity].Traded)
            {
                return Result.CreateGenericFailureResult("Cannot sell commodities if it is not traded.");
            }
            double units = Command.GetUnits(command);
            if (units <= 0.0)
            {
                return Result.CreateGenericFailureResult("Must sell a positive number of units.");
            }
            if (units > Avatar.Inventory[commodity].Units)
            {
                return Result.CreateGenericFailureResult("You don't have that many units to sell.");
            }
            Island island = Islands[Avatar.DockedIslandID];
            double unitPrice = island.Commodities[commodity].SellUnitPrice;
            double totalPrice = unitPrice * units;
            island.Commodities[commodity].SellUnits(units);
            Avatar.Inventory[commodity].Units -= units;
            Avatar.Inventory[Config.CurrencyCommodity].Units += totalPrice;
            return Result.CreateGenericSuccessResult();
        }
        private XElement GetShipInformationCommandHandler(XElement command)
        {
            return Result.CreateShipInformationResult(Ships[Command.GetShip(command)]);
        }
        private XElement GetAvatarSpeedListCommandHandler(XElement command)
        {
            return Result.CreateAvatarSpeedListResult(Config.AvatarSpeeds);
        }
        private XElement GetCommodityInformationCommandHandler(XElement command)
        {
            return Result.CreateCommodityInformationResult(Commodities[Command.GetCommodity(command)]);
        }
        private XElement SetBoundForCommandHandler(XElement command)
        {
            if (Avatar.State != AvatarState.AtSea)
            {
                return Result.CreateGenericFailureResult("Cannot set bound for unless the avatar is at sea.");
            }
            Avatar.BoundForIslandID = Command.GetIslandId(command);
            if (Avatar.BoundForIslandID != Config.NullIslandId)
            {
                Island island = Islands[Avatar.BoundForIslandID];
                Location delta = new Location(island.Position.X - avatar.Position.X, island.Position.Y - avatar.Position.Y);
                Avatar.Direction.Degrees = delta.ToHeading().Degrees;
            }
            return Result.CreateGenericSuccessResult();
        }
        private XElement GetKnownIslandListCommandHandler(XElement command)
        {
            List<int> islandList = new List<int>();
            foreach (int islandId in Avatar.KnownIslands.Keys)
            {
                islandList.Add(islandId);
            }
            return Result.CreateIslandListResult(islandList.ToArray());
        }
        private XElement StartGameCommandHandler(XElement command)
        {
            if (Avatar.State != AvatarState.Uninitialized)
            {
                return Result.CreateGenericFailureResult( "Cannot start a game unless avatar is in an uninitialized state.");
            }
            islands = new Islands(this);
            avatar = new Avatar(this);
            return Result.CreateGenericSuccessResult();
        }
        private XElement GetAvatarInformationCommandHandler(XElement command)
        {
            return Result.CreateAvatarInformationResult(Avatar);
        }
        private XElement SetAvatarSpeedCommandHandler(XElement command)
        {
            if (Avatar.State != AvatarState.AtSea)
            {
                return Result.CreateGenericFailureResult("Cannot set speed unless the avatar is at sea.");
            }
            string avatarSpeed = Command.GetAvatarSpeed(command);
            avatar.Speed = avatarSpeed;
            return Result.CreateGenericSuccessResult();
        }
        private XElement SetHeadingCommandHandler(XElement command)
        {
            if (Avatar.State != AvatarState.AtSea)
            {
                return Result.CreateGenericFailureResult("Cannot set heading unless the avatar is at sea.");
            }
            double heading = Command.GetHeading(command);
            avatar.Direction.Degrees = heading;
            avatar.BoundForIslandID = Config.NullIslandId;
            return Result.CreateGenericSuccessResult();
        }
        private XElement GetAvatarSpeedInformationCommandHandler(XElement command)
        {
            string speed = Command.GetAvatarSpeed(command);
            return Result.CreateAvatarSpeedInformationResult(Config.AvatarSpeeds[speed]);
        }
        private XElement GetIslandInformationCommandHandler(XElement command)
        {
            int islandId = Command.GetIslandId(command);
            Island island = Islands[islandId];
            AvatarIsland avatarIsland = Avatar.KnownIslands[islandId];
            return Result.CreateIslandInformationResult(island, avatarIsland, Config, Avatar);
        }
        private void LocateIslands()
        {
            int[] islandList = GetIslandListByDistanceFromLocation(avatar.Position, config.MaximumLocationDistance);
            foreach (int island in islandList)
            {
                avatar.LocateIsland(island);
            }
        }
        private XElement UndockCommandHandler(XElement command)
        {
            if (Avatar.State != AvatarState.Docked)
            {
                return Result.CreateGenericFailureResult("Cannot undock unless the avatar is docked.");
            }
            avatar.State = AvatarState.AtSea;
            avatar.DaysAtSea = 0;
            avatar.Speed = Config.InitialSpeed;
            return Result.CreateGenericSuccessResult();
        }
        private XElement DockCommandHandler(XElement command)
        {
            if (Avatar.State != AvatarState.AtSea)
            {
                return Result.CreateGenericFailureResult("Cannot dock unless the avatar is at sea.");
            }
            int islandId = Command.GetIslandId(command);
            if (islands[islandId].Position.DistanceTo(avatar.Position) > config.MaximumDockingDistance)
            {
                return Result.CreateGenericFailureResult("That island is too far away from the avatar to dock with.");
            }
            avatar.State = AvatarState.Docked;
            avatar.DockedIslandID = islandId;
            avatar.BoundForIslandID = Config.NullIslandId;
            avatar.VisitIsland(islandId);
            return Result.CreateGenericSuccessResult();
        }
        private XElement ToggleBookmarkIslandCommandHandler(XElement command)
        {
            int theIslandID = Command.GetIslandId(command);
            avatar.ToggleIslandBookMark(theIslandID);
            return Result.CreateGenericSuccessResult();
        }
        private XElement AnnotateIslandCommandHandler(XElement command)
        {
            int theIslandID = Command.GetIslandId(command);
            string theNote = string.Empty;
            theNote = Command.GetNotes(command);
            avatar.AnnotateIsland(theIslandID, theNote);
            return Result.CreateGenericSuccessResult();
        }
        private XElement GetDockableIslandListCommandHandler(XElement command)
        {
            if (Avatar.State != AvatarState.AtSea)
            {
                return Result.CreateGenericFailureResult("Cannot get dockable island list unless the avatar is at sea.");
            }
            return Result.CreateIslandListResult(GetIslandListByDistanceFromLocation(avatar.Position,config.MaximumDockingDistance));
        }
        private XElement GetNearByIslandListCommandHandler(XElement command)
        {
            if (Avatar.State != AvatarState.AtSea)
            {
                return Result.CreateGenericFailureResult("Cannot get located island list unless the avatar is at sea.");
            }
            LocateIslands();
            return Result.CreateIslandListResult(GetIslandListByDistanceFromLocation(avatar.Position, config.MaximumLocationDistance));
        }
        private XElement SailCommandHandler(XElement command)
        {
            if (Avatar.State != AvatarState.AtSea)
            {
                return Result.CreateGenericFailureResult("Cannot sail unless the avatar is at sea.");
            }
            avatar.Position = new Location(Avatar.Position.X + Config.AvatarSpeeds[Avatar.Speed].Factor * Avatar.Direction.ToLocation().X, Avatar.Position.Y + Config.AvatarSpeeds[Avatar.Speed].Factor * Avatar.Direction.ToLocation().Y);
            avatar.Day++;
            avatar.DaysAtSea++;
            if (avatar.BoundForIslandID != Config.NullIslandId)
            {
                Island island = Islands[avatar.BoundForIslandID];
                if (island.Position.DistanceTo(avatar.Position) <= Config.MaximumDockingDistance)
                {
                    avatar.Speed = Config.InitialSpeed;
                    avatar.BoundForIslandID = Config.NullIslandId;
                }
            }
            return Result.CreateGenericSuccessResult();
        }
        #endregion
        #region Helper Functions
        private int[] GetIslandListByDistanceFromLocation(ILocation location, double maximumDistance)
        {
            return GetIslandListByDistanceFromLocation(location, maximumDistance, 0);
        }
        private int[] GetIslandListByDistanceFromLocation(ILocation location, double maximumDistance, double minimumDistance)
        {
            return GetIslandListByDaysFromLocationAtSpeed(location, maximumDistance, minimumDistance, 1.0);
        }
        private int[] GetIslandListByDaysFromLocationAtSpeed(ILocation location, double maximumDistance, double minimumDistance, double speed)
        {
            List<int> islandList = new List<int>();
            foreach (int islandID in islands.Keys)
            {
                double distance = Location.Distance(location, islands[islandID].Position);
                if (distance >= minimumDistance && distance < maximumDistance)
                {
                    islandList.Add(islandID);
                }
            }
            return islandList.ToArray();
        }
        #endregion
    }
    public static class GameFactory
    {
        private const string defaultGameFile = "./data/template.xml";
        public static IGame Create()
        {
            return Create(defaultGameFile);
        }
        public static IGame Create(string gameFile)
        {
            XDocument doc = XDocument.Load(gameFile);
            return new Game(doc.Element(GameXNames.game));
        }
        public static void Save(IGame game, string gameFile)
        {
            XDocument doc = new XDocument();
            doc.Add(game.ToXElement(GameXNames.game));
            doc.Save(gameFile);
        }
    }
}
